using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Net.Sockets;
using System.Net.Security;
using System.Globalization;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Irc.Events;
using Irc.Objects;

namespace Irc
{
	[CLSCompliant(true)]
	public class Client : IDisposable
	{
		#region public attributes
		public Servername Server {
			get { return server; }
			internal set { server = value; }
		}
		public Nickname Nickname {
			get { return nickname; }
			internal set
			{
				OnNickChange(nickname, value);
				nickname = value;
			}
		}
		public string Password {
			get { return password; }
			internal set { password = value; }
		}
		public bool Debug {
			get { return debug; }
			set { debug = value; }
		}
		public Dictionary<string,Channel> Channels {
			get { return joinedChannels; }
		}
		public Motd Motd {
			get { return motd; }
		}
		public bool Registered
		{
			get { return registered; }
		}

		public RemoteCertificateValidationCallback SslCertValidation {
			get { return Sslcertvalidator; }
			set { Sslcertvalidator = value; }
		}
		#endregion
		#region events
		public event EventHandler<JoinEventArgs> UserJoin;
		public event EventHandler<JoinEventArgs> SelfJoin;
		public event EventHandler<TopicEventArgs> TopicChange;
		public event EventHandler<ModeEventArgs> ModeChange;
		public event EventHandler<NickEventArgs> NickChange;
		public event EventHandler<MessageEventArgs> PublicMessage;
		public event EventHandler<MessageEventArgs> PublicNotice;
		public event EventHandler<MessageEventArgs> PublicAction;
		public event EventHandler<MessageEventArgs> PrivateMessage;
		public event EventHandler<MessageEventArgs> PrivateNotice;
		public event EventHandler<MessageEventArgs> PrivateAction;
		public event EventHandler<ServerEventArgs> ServerNotice;
		public event EventHandler<PingEventArgs> Ping;
		public event EventHandler<MessageEventArgs> DebugMessage;
		public event EventHandler<ConnectEventArgs> Connecting;
		public event EventHandler<ConnectEventArgs> Connected;
		public event EventHandler<ConnectEventArgs> Disconnecting;
		public event EventHandler<ConnectEventArgs> Disconnected;
		public event EventHandler<MotdEventArgs> MotdReady;
		public event EventHandler<CtcpEventArgs> CtcpMessage;
		public event EventHandler<ServerEventArgs> ErrorMessage;
		public event EventHandler<NicklistEventArgs> Nicklist;
		#endregion
		#region event creators
		protected void OnSelfJoin(UserMove movement)
		{
			JoinEventArgs e = new JoinEventArgs();
			e.Movement = movement;
			if(e.Movement.Time == DateTime.MinValue)
				e.Movement.Time = DateTime.Now;
			if(SelfJoin != null)
				new Thread(delegate() { SelfJoin(this, e); }).Start();
		}
		protected void OnUserJoin(UserMove movement)
		{
			JoinEventArgs e = new JoinEventArgs();
			e.Movement = movement;
			if(e.Movement.Time == DateTime.MinValue)
				e.Movement.Time = DateTime.Now;
			if(UserJoin != null)
				new Thread(delegate() { UserJoin(this, e); }).Start();
			new Thread(delegate(){((Channel)e.Movement.Target).AddJoin(movement);}).Start();
		}
		protected void OnTopicChange(Topic topic)
		{
			TopicEventArgs e = new TopicEventArgs();
			e.Topic = topic;
			if(TopicChange != null)
				new Thread(delegate() { TopicChange(this, e); }).Start();
			new Thread(delegate(){((Channel)topic.Target).AddTopic(topic);}).Start();
		}
		protected void OnModeChange(IModeMessage mode)
		{
			ModeEventArgs e = new ModeEventArgs();
			e.Mode = mode;
			e.Mode.Time = DateTime.Now;
			if(ModeChange != null)
				new Thread(delegate() { ModeChange(this, e); }).Start();
			if(mode.Mode.Target is Channel)
				new Thread(delegate() { ((Channel)mode.Mode.Target).AddModeChange(mode); }).Start();
		}
		protected void OnNickChange(Nickname oldnick, Nickname newnick)
		{
			NickEventArgs e = new NickEventArgs();
			e.OldNick = oldnick;
			e.NewNick = newnick;
			e.Time = DateTime.Now;
			if(NickChange != null)
				new Thread(delegate(){NickChange(this, e);}).Start();
			foreach(KeyValuePair<string,Channel> c in Channels)
			{
				new Thread(delegate(){c.Value.ChangeNick(oldnick, newnick);}).Start();
			}
		}
		protected void OnPublicMessage(PublicMessage message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PublicMessage != null)
				new Thread(delegate(){PublicMessage(this, e);}).Start();
			new Thread(delegate(){((Channel)message.Target).OnPublicMessage(message);}).Start();
		}
		protected void OnPublicNotice(PublicNotice message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PublicNotice != null)
				new Thread(delegate(){PublicNotice(this, e);}).Start();
			new Thread(delegate(){((Channel)message.Target).OnPublicNotice(message);}).Start();
		}
		protected void OnPublicAction(PublicAction message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PublicAction != null)
				new Thread(delegate(){PublicAction(this, e);}).Start();
			new Thread(delegate(){((Channel)message.Target).OnPublicAction(message);}).Start();
		}
		protected void OnPrivateMessage(PrivateMessage message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PrivateMessage != null)
				new Thread(delegate(){PrivateMessage(this, e);}).Start();
		}
		protected void OnPrivateNotice(PrivateNotice message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PrivateNotice != null)
				new Thread(delegate(){PrivateNotice(this, e);}).Start();
		}
		protected void OnPrivateAction(PrivateAction message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(PrivateAction != null)
				new Thread(delegate(){PrivateAction(this, e);}).Start();
		}
		protected void OnServerNotice(ServerMessage message)
		{
			ServerEventArgs e = new ServerEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(ServerNotice != null)
				new Thread(delegate(){ServerNotice(this, e);}).Start();
		}
		protected void OnPing(string key)
		{
			PingEventArgs e = new PingEventArgs();
			e.Key = key;
			if(Ping != null)
				Ping(this, e);
		}
		protected void OnDebugMessage(Message message)
		{
			MessageEventArgs e = new MessageEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(Debug && DebugMessage != null)
				new Thread(delegate(){DebugMessage(this, e);}).Start();
		}
		protected void OnConnecting(Servername server)
		{
			ConnectEventArgs e = new ConnectEventArgs();
			e.Server = server;
			e.Cancel = true;
			e.Time = DateTime.Now;
			if(Connecting != null)
				Connecting(this, e);
		}
		protected void OnDisconnecting(Servername server)
		{
			ConnectEventArgs e = new ConnectEventArgs();
			e.Server = server;
			e.Time = DateTime.Now;
			e.Cancel = false;
			if(Disconnecting != null)
				Disconnecting(this, e);
		}
		protected void OnConnected(Servername server)
		{
			ConnectEventArgs e = new ConnectEventArgs();
			e.Server = server;
			e.Time = DateTime.Now;
			e.Cancel = false;
			if(Connected != null)
				new Thread(delegate(){Connected(this, e);}).Start();
		}
		protected void OnDisconnected(Servername server)
		{
			ConnectEventArgs e = new ConnectEventArgs();
			e.Server = server;
			e.Time = DateTime.Now;
			e.Cancel = false;
			if(Disconnected != null)
				new Thread(delegate() { Disconnected(this, e); }).Start();
		}
		protected void OnMotdReady()
		{
			MotdEventArgs e = new MotdEventArgs();
			e.Motd = motd;
			e.Motd.Time = DateTime.Now;
			if(MotdReady != null)
				new Thread(delegate(){MotdReady(this, e);}).Start();
		}
		protected void OnCtcpMessage(Ctcp message)
		{
			CtcpEventArgs e = new CtcpEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(CtcpMessage != null)
				new Thread(delegate(){CtcpMessage(this, e);}).Start();
		}
		protected void OnError(ServerMessage message)
		{
			ServerEventArgs e = new ServerEventArgs();
			e.Message = message;
			if(e.Message.Time == DateTime.MinValue)
				e.Message.Time = DateTime.Now;
			if(ErrorMessage != null)
				new Thread(delegate() { ErrorMessage(this, e); }).Start();
		}
		protected void OnNicklist(Nicklist nicklist)
		{
			NicklistEventArgs e = new NicklistEventArgs();
			e.Nicklist = nicklist;
			e.Nicklist.Time = DateTime.Now;
			if(Nicklist != null)
				new Thread(delegate(){Nicklist(this, e);}).Start();
			new Thread(delegate(){((Channel)nicklist.Target).OnJoinNicklist(nicklist);}).Start();
		}
		#endregion
		#region private attributes
		private TcpClient connector;
		private StreamReader reader;
		private StreamWriter writer;
		private Thread connectorThread;
		private Dictionary<string,Channel> joinedChannels = new Dictionary<string,Channel>();
		private Servername server;
		private Nickname nickname;
		private string password;
		private Motd motd = new Motd();
		private RemoteCertificateValidationCallback Sslcertvalidator;
		private bool abort;
		private bool debug;
		private bool registered;
		#endregion
		#region public methods
		/// <summary>
		/// Creates an IRC Client object
		/// </summary>
		/// <param name="server">A Servername object with the server data to connect to</param>
		/// <param name="nickname">Your nickname</param>
		/// <param name="pass">The password to connect with</param>
		public Client(Servername server, Nickname nickname, string pass)
		{
			Server = server;
			this.nickname = nickname;
			Password = pass ?? String.Empty;
			OnConnecting(Server);
		}
		/// <summary>
		/// Connect to an IRC server
		/// </summary>
		public void Connect()
		{
			connectorThread = new Thread(ConnectThread);
			connectorThread.Start();
		}
		/// <summary>
		/// Disconnect with the standard quit message
		/// </summary>
		public void Disconnect()
		{
			Disconnect(Strings.DefaultQuitMessage);
		}
		/// <summary>
		/// Disconnect with your own quit message
		/// </summary>
		/// <param name="message">The message to quit with</param>
		public void Disconnect(string message)
		{
			OnDisconnecting(Server);
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.QuitMessage, message));
			abort = true;
			if(connectorThread != null && connectorThread.IsAlive)
				connectorThread.Abort();
		}
		/// <summary>
		/// Send a notice to a target
		/// </summary>
		/// <param name="message">A Message object containing the data needed to send the notice</param>
		public void Notice(Message message)
		{
			if(message == null)
				return;
			if(message is PublicNotice || message is PrivateNotice || message is ServerNotice)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.NoticeMessage, message.Target, message.Text));
			message.Sender = Nickname;
			message.Time = DateTime.Now;
			if(message is PublicNotice)
				OnPublicNotice(((PublicNotice)message));
			else if(message is PrivateNotice)
				OnPrivateNotice(((PrivateNotice)message));
			else if(message is ServerNotice)
				OnServerNotice(((ServerMessage)message));
		}
		/// <summary>
		/// Send a message to a target
		/// </summary>
		/// <param name="message">A Message object containing the data needed to send the message</param>
		public void Say(Message message)
		{
			if(message == null)
				return;
			if(message is PublicMessage || message is PrivateMessage || message is ServerMessage)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.PrivateMessage, message.Target, message.Text));
			message.Sender = Nickname;
			message.Time = DateTime.Now;
			if(message is PublicMessage)
				OnPublicMessage((PublicMessage)message);
			else if(message is PrivateMessage)
				OnPrivateMessage((PrivateMessage)message);
		}
		/// <summary>
		/// Sends an action to a target
		/// </summary>
		/// <param name="message">A Message object containing the data needed to send the message</param>
		public void Act(Message message)
		{
			if(message == null)
				return;
			if(message is PublicAction || message is PrivateAction)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.PrivateMessage, message.Target, 
							String.Format(CultureInfo.CurrentCulture, Strings.ActionMessage, message.Text)));
			message.Sender = Nickname;
			message.Time = DateTime.Now;
			if(message is PublicAction)
				OnPublicAction((PublicAction)message);
			else
				OnPrivateAction((PrivateAction)message);
		}
		/// <summary>
		/// Sends a Ctcp Version message to a target
		/// </summary>
		/// <param name="target">The target to send the message to</param>
		public void Version(Target target)
		{
			Ctcp message = new Ctcp();
			message.Target = target;
			message.Text = @"version";
			Ctcp(message);
		}
		/// <summary>
		/// Sends a Ctcp message to a target
		/// </summary>
		/// <param name="message">A Ctcp object containing the data needed to send the Ctcp</param>
		public void Ctcp(Ctcp message)
		{
			if(message == null)
				return;
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.CtcpMessage,
					message.Target, message.Text.ToUpperInvariant(),
					(!String.IsNullOrEmpty(message.Parameters) ? " "+message.Parameters : "")));
		}
		/// <summary>
		/// Sends a Ctcp Reply to a target
		/// </summary>
		/// <param name="message">A Ctcp object containing the data needed to send the Ctcp Reply</param>
		public void CtcpReply(Ctcp message)
		{
			if(message == null)
				return;
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.NoticeMessage,
				message.Target,
					String.Format(CultureInfo.CurrentCulture, Strings.NilEscapedMessage,
						message.Text + " " + message.Parameters)));
		}
		/// <summary>
		/// Joins a channel
		/// </summary>
		/// <param name="channel">A string containing the name of the channel to join</param>
		public void JoinChannel(string channel)
		{
			if(Channels.ContainsKey(channel))
				Channels[channel].Rejoin();
			else
			{
				string message = Strings.JoinWithoutPassMessage;
				SendMessage(String.Format(System.Globalization.CultureInfo.CurrentCulture, message, channel));
			}
		}
		/// <summary>
		/// Joins a passworded channel
		/// </summary>
		/// <param name="channel">A string containing the name of the channel to join</param>
		/// <param name="key">A string containing the password for the channel</param>
		public void JoinChannel(string channel, string key)
		{
			if(Channels.ContainsKey(channel))
				Channels[channel].Rejoin();
			else
			{
				string message = Strings.JoinWithPassMessage;
				SendMessage(String.Format(System.Globalization.CultureInfo.CurrentCulture, message, channel, key));
			}
		}
		/// <summary>
		/// Changes your nickname
		/// </summary>
		/// <param name="newnick">A Nickname object containing the new Nickname you'd like to use</param>
		public void ChangeNick(Nickname newnick)
		{
			if(newnick == null)
				return;
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.NickMessage, newnick.Name));
			Nickname = newnick;
		}
		/// <summary>
		/// Change the topic in a channel
		/// </summary>
		/// <param name="topic">A Topic object containing the topic data you wish to set</param>
		public void ChangeTopic(Topic topic)
		{
			if(topic == null)
				return;
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.TopicMessage, topic.Target, topic.Text));
			topic.Time = DateTime.Now;
			topic.Sender = Nickname;
			OnTopicChange(topic);
		}
		/// <summary>
		/// Change a mode somewhere
		/// </summary>
		/// <param name="message">A ModeMessaage object containing the mode change data</param>
		public void ChangeMode(IModeMessage message)
		{
			string mode = (message is IModeAdd ? '+' : '-') + "" + ModeMap.Instance.ModeToCharTable[message.Mode.ModeType];
			NicknameMode nm = message.Mode as NicknameMode;
			StringMode sm = message.Mode as StringMode;
			IntMode im = message.Mode as IntMode;
			if(nm != null)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.ModeWithParamsMessage,
							message.Mode.Target.ToString(), mode, nm.Parameter));
			else if(sm != null)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.ModeWithParamsMessage,
							message.Mode.Target.ToString(), mode, sm.Parameter));
			else if(im != null)
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.ModeWithParamsMessage,
							message.Mode.Target.ToString(), mode, im.Parameter));
			else
				SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.ModeWithoutParamsMessage,
							message.Mode.Target.ToString(), mode));
		}
		/// <summary>
		/// Send a raw message to the server
		/// </summary>
		/// <param name="message">The raw message to send</param>
		public void SendMessage(string message)
		{
			if(connector != null)
			{
				if(connector.Connected)
				{
					lock(connector)
					{
						Message m = new Message();
						m.Text = message;
						OnDebugMessage(m);
						writer.WriteLine(message);
						writer.Flush();
						Thread.Sleep(100);
					}
				}
				else
				{
					throw new SocketException((int)SocketError.NotConnected);
				}
			}
		}
		#endregion
		#region private methods
		private void ConnectThread()
		{
			try {
				connector = new TcpClient(Server.Hostmask, Server.Port);
			} catch (Exception) {
				throw new System.Net.WebException(String.Format(CultureInfo.CurrentCulture, Strings.ServerFailedError, Server.Hostmask, Server.Port), WebExceptionStatus.ConnectFailure);
			} finally {
				if(!connector.Connected)
					Thread.CurrentThread.Abort();
			}
			Stream s = Stream.Null;
			if(Server.Ssl) {
				try {
					SslStream temp = new SslStream(connector.GetStream(), false, (Sslcertvalidator ?? AutoCertHandler));
					temp.AuthenticateAsClient(String.Empty, null, SslProtocols.Tls | SslProtocols.Ssl3, true);
					s = temp;
				} catch (IOException) {
					throw new ArgumentException(Strings.SslPortError);
				} finally {
					if(!connector.Connected)
						Thread.CurrentThread.Abort();
				}
			} else {
				s = connector.GetStream();
			}
			reader = new StreamReader(s);
			writer = new StreamWriter(s);
			try {
				RegisterConnection();
			} catch (SocketException e) {
				Message m = new Message();
				m.Sender = Server;
				m.Text = e.ToString();
				m.Time = DateTime.Now;
				OnDebugMessage(m);
				throw;
			} finally {
				if(!connector.Connected)
					Thread.CurrentThread.Abort();
			}
			object sharedData = null;

			Ping += AutoPingResponder;
			while(!abort)
			{
				try {
					string message = reader.ReadLine();
					if(message != null)
						ParseMessage(message, ref sharedData);
					Thread.Sleep(100);
				} catch (SocketException e) {
					Message m = new Message();
					m.Sender = Server;
					m.Text = e.ToString();
					m.Time = DateTime.Now;
					OnDebugMessage(m);
					throw;
				} catch (IOException e) {
					Message m = new Message();
					m.Sender = Server;
					m.Text = e.ToString();
					m.Time = DateTime.Now;
					OnDebugMessage(m);
					throw;
				} finally {
					if(!connector.Connected)
						Thread.CurrentThread.Abort();
				}
			}
			OnDisconnected(Server);
			reader.Close();
			s.Close();
		}
		private void ParseMessage(string message, ref object spareData)
		{
			#region parsing
			Message m = new Message();
			m.Text = message;
			m.Time = DateTime.Now;
			OnDebugMessage(m);
			Nickname nick = null;
			string server = String.Empty;
			string[] messageParts = message.Split(new char[]{' '}, 4);
			if(messageParts.Length == 2)
			{
				// ping/pong event, so we just parse it and be done since it's a short and simple message
				OnPing(messageParts[1].Substring(1));
			}
			if(messageParts.Length >= 3)
			{
				// messages with 3 or more parts have a hostmask as the first part (or a server if not a user, or just the name of the target, in the case of self mode messages)
				string name, real, host;
				if(messageParts[0].IndexOf('@') != -1) {
					int index1 = messageParts[0].IndexOf('!'), index2 = messageParts[0].IndexOf('@');
					name = messageParts[0].Substring(1, index1-1);
					real = messageParts[0].Substring(index1+1, index2-index1-1);
					host = messageParts[0].Substring(index2+1);
					nick = new Nickname(name, real, host);
				} else if(messageParts[0].IndexOf('.') != -1) {
					server = messageParts[0].Substring(1);
				} else {
					nick = new Nickname(messageParts[0]);
				}
			}
			#endregion
			if(messageParts.Length == 3)
			{
				string action = messageParts[1].ToLowerInvariant(), target = messageParts[2];
				target = target.Substring((target[0] == ':' ? 1 : 0));
				#region nick changes
				if(action.CompareTo("nick") == 0)
				{
					Nickname newnick = new Nickname(nick.Name, nick.Realname, nick.Hostmask);
					newnick.Name = target;
					OnNickChange(nick, newnick);
				}
				#endregion
				#region channel joins/parts
				else if(action.CompareTo("join") == 0)
				{
					Channel channel = null;
					if(nick.Equals(nickname))
					{
						if(!Channels.TryGetValue(target, out channel))
							Channels[target] = channel = new Channel(this, target);
						UserJoin j = new UserJoin();
						j.Target = channel;
						j.Time = DateTime.Now;
						j.User = Nickname;
						OnSelfJoin(j);
					}
					else
					{
						Channels.TryGetValue(target, out channel);
						UserJoin j = new UserJoin();
						j.Target = channel;
						j.Time = DateTime.Now;
						j.User = nick;
						OnUserJoin(j);
					}
				}
				else if(action.CompareTo("part") == 0)
				{
					Channel channel = null;
					if(nick.Equals(nickname))
					{
						Channels.TryGetValue(target, out channel);
						if(channel != null)
							Channels.Remove(target);
						UserPart p = new UserPart();
						p.Target = channel;
						p.Time = DateTime.Now;
						p.User = Nickname;
						OnSelfJoin(p);
					}
					else
					{
						Channels.TryGetValue(target, out channel);
						UserPart p = new UserPart();
						p.Target = channel;
						p.Time = DateTime.Now;
						p.User = nick;
						OnUserJoin(p);
					}
				}
				#endregion
				else
				{
					Message msg = new Message();
					msg.Time = DateTime.Now;
					msg.Text = "Invalid message?";
					OnDebugMessage(msg);
				}
			}
			if(messageParts.Length >= 4)
			{
				string action = messageParts[1].ToLowerInvariant();
				if(messageParts[0].ToLowerInvariant().CompareTo("error") == 0)
				{
					ServerMessage sm = new ServerMessage();
					sm.Text = String.Join(" ", messageParts, 1, messageParts.Length-1).Substring(1);
					sm.Time = DateTime.Now;
					sm.Sender = Server;
					OnError(sm);
				}
				else if(server.Length > 0)
				{
					#region server messages
					if(action.CompareTo("privmsg") == 0 || action.CompareTo("notice") == 0)
					{
						string serverMessage = messageParts[3].Substring(1);
						ServerMessage sm = new ServerMessage();
						sm.Sender = new Servername(server);
						sm.Time = DateTime.Now;
						sm.Text = serverMessage;
						OnServerNotice(sm);
					}
					#endregion
					else if(action.CompareTo("001") == 0)
					{
						nickname = Nickname.Parse(messageParts[3].Substring(messageParts[3].LastIndexOf(' ')+1));
						ServerMessage sm = new ServerMessage();
						sm.Sender = new Servername(server);
						sm.Time = DateTime.Now;
						sm.Text = "You are now connected!";
						OnServerNotice(sm);
						OnConnected(Server);
					}
					else if(action.CompareTo("002") == 0)
					{
					}
					else if(action.CompareTo("003") == 0)
					{
					}
					else if(action.CompareTo("004") == 0)
					{
					}
					else if(action.CompareTo("005") == 0)
					{
					}
					else if(action.CompareTo("251") == 0)
					{
						
					}
					#region random server messages
					else if(action.CompareTo("252") == 0 || action.CompareTo("254") == 0 ||
					        action.CompareTo("255") == 0 || action.CompareTo("265") == 0 ||
					        action.CompareTo("266") == 0)
					{
						// TODO: These messages need more parsing. Grr.
						ServerMessage sm = new ServerMessage();
						sm.Sender = Server;
						sm.Time = DateTime.Now;
						sm.Text = message.Split(new char[]{' '}, 4)[3];
						OnServerNotice(sm);
					}
					#endregion
					else if(action.CompareTo("253") == 0)
					{
					}
					#region motd (372/375/376)
					else if(action.CompareTo("375") == 0)
					{
						motd.Message = String.Empty;
					}
					else if(action.CompareTo("372") == 0)
					{
						motd.Message += messageParts[3].Substring(2) + "\r\n";
					}
					else if(action.CompareTo("376") == 0)
					{
						OnMotdReady();
					}
					#endregion
					#region on join topic data
					else if(action.CompareTo("331") == 0 || action.CompareTo("332") == 0)
					{
						spareData = new Topic();
						Topic args = spareData as Topic;
						Channel channel = null;
						string chName = messageParts[3].Split(new char[]{' '}, 2)[0];
						if(!Channels.TryGetValue(chName, out channel))
							Channels[chName] = channel = new Channel(this, messageParts[2]);
						args.Target = Channels[chName];
						args.Text = messageParts[3].Split(new char[]{' '}, 2)[1].Substring(1);
					}
					else if(action.CompareTo("333") == 0)
					{
						Topic args = spareData as Topic;
						string[] topicParts = messageParts[3].Split(' ');
						DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(Convert.ToDouble(topicParts[2], CultureInfo.CurrentCulture));
						args.Sender = new Nickname(topicParts[1]);
						args.Time = dateTime;
						OnTopicChange(args);
						spareData = null;
					}
					#endregion
					#region on join nicklist
					else if(action.CompareTo("353") == 0)
					{
						// nicklist
						if(spareData == null)
							spareData = new Nicklist();
						Nicklist args = spareData as Nicklist;
						string nicklist = messageParts[3];
						string[] parts = nicklist.Split(' ');
						Channel channel = null;
						Channels.TryGetValue(parts[1], out channel);
						args.Target = channel;
						List<Nickname> nicks = new List<Nickname>();
						nicks.AddRange(args.Nicknames);
						for(int i = 2; i < parts.Length; i++)
						{
							string name = parts[i];
							if(String.IsNullOrEmpty(name))
								continue;
							if(name.StartsWith(":"))
								name = name.Substring(1);
							string temp = name.ToUpperInvariant();
							List<ModeName> tmpmode = new List<ModeName>();
							while(Char.IsSymbol(temp[0]) && temp[0] != '[' && temp[0] != ']')
							{
								tmpmode.Add(ModeMap.Instance.SymbolToModeTable[name[0]]);
								name = name.Substring(1);
								temp = name.ToUpperInvariant();
							}
							nicks.Add(new Nickname(name, tmpmode.ToArray()));
						}
						args.Nicknames = new Collection<Nickname>(nicks);
					}
					else if(action.CompareTo("366") == 0)
					{
						// end of nicklist
						Nicklist args = spareData as Nicklist;
						OnNicklist(args);
						spareData = null;
					}
					#endregion
					else if(action.CompareTo("439") == 0)
					{
						// spam zombie trap
					}
					else
					{
						Message msg = new Message();
						msg.Text = "Invalid message?";
						msg.Time = DateTime.Now;
						OnDebugMessage(msg);
					}
				}
				#region mode change
				else if(action.CompareTo("mode") == 0)
				{
					IModeMessage mode = null;
					string[] modeParts = messageParts[3].Split(' ');
					string modes = modeParts[0];
					if(modes.StartsWith(":"))
						modes = modes.Substring(1);
					string[] parameters = new string[(modeParts.Length - 1)];
					Array.Copy(modeParts, 1, parameters, 0, parameters.Length);
					if(messageParts[2].StartsWith("#") || messageParts[2].StartsWith("&"))
					{
						// channel mode
						Channel channel;
						if(!Channels.TryGetValue(messageParts[2], out channel))
							Channels[messageParts[2]] = channel = new Channel(this, messageParts[2]);
						int i = 0;
						foreach(char c in modes)
						{
							switch(c)
							{
								case '+': mode = new ChannelModeAdd(); mode.Sender = nick; break;
								case '-': mode = new ChannelModeRemove(); mode.Sender = nick; break;
								default:
									if((modes.Length - i) == parameters.Length)
									{
										int result;
										Nickname tmpnick;
										if(Int32.TryParse(parameters[(modes.Length-i-1)], out result))
											mode.Mode = new IntMode(channel, ModeMap.Instance.CharToModeTable[c], result);
										else if(Nickname.TryParse(parameters[(modes.Length-i-1)], out tmpnick))
											mode.Mode = new NicknameMode(channel, ModeMap.Instance.CharToModeTable[c], tmpnick);
										else
											mode.Mode = new StringMode(channel, ModeMap.Instance.CharToModeTable[c], parameters[(modes.Length-i-1)]);
										OnModeChange(mode);
									}
									else
									{
										mode.Mode = new Mode(channel, ModeMap.Instance.CharToModeTable[c]);
										OnModeChange(mode);
									}
									break;
							}
							i++;
						}
					}
					else
					{
						// user mode
						Nickname user = new Nickname(messageParts[2]);
						int i = 0;
						foreach(char c in modes)
						{
							switch(c)
							{
								case '+': mode = new NickModeAdd(); mode.Sender = nick; break;
								case '-': mode = new NickModeRemove(); mode.Sender = nick; break;
								default:
									if((modes.Length - i) == parameters.Length)
									{
										int result;
										Nickname tmpnick;
										if(Int32.TryParse(parameters[(modes.Length-i-1)], out result))
											mode.Mode = new IntMode(user, ModeMap.Instance.CharToModeTable[c], result);
										else if(Nickname.TryParse(parameters[(modes.Length-i-1)], out tmpnick))
											mode.Mode = new NicknameMode(user, ModeMap.Instance.CharToModeTable[c], tmpnick);
										else
											mode.Mode = new StringMode(user, ModeMap.Instance.CharToModeTable[c], parameters[(modes.Length-i-1)]);
										OnModeChange(mode);
									}
									else
									{
										mode.Mode = new Mode(user, ModeMap.Instance.CharToModeTable[c]);
										OnModeChange(mode);
									}
									break;
							}
						}
						i++;
					}
				}
				#endregion
				#region private/public/ctcp/action messages
				else if(action.CompareTo("privmsg") == 0)
				{
					string target = messageParts[2];
					string text = String.Join(" ", messageParts, 3, messageParts.Length - 3).Substring(1);
					if(target.StartsWith("#") || target.StartsWith("&"))
					{
						Channel channel;
						if(!Channels.TryGetValue(target, out channel))
							Channels[target] = channel = new Channel(this, target);
						if(text.StartsWith("\x01"))
						{
							int index = text.IndexOf(' ', 1);
							index = index == -1 ? text.Length-1 : index;
							string type = text.Substring(1, index-1).ToLowerInvariant();
							string messageText = String.Empty;
							if(index != text.Length-1)
								messageText = text.Substring(index, text.Length-index-1);
							if(type.CompareTo("action") == 0)
							{
								PublicAction pa = new PublicAction();
								pa.Target = channel;
								pa.Sender = nick;
								pa.Text = messageText;
								pa.Time = DateTime.Now;
								OnPublicAction(pa);
							}
							else
							{
								Ctcp ctcp = new Ctcp();
								ctcp.Time = DateTime.Now;
								ctcp.Sender = nick;
								ctcp.Text = type;
								ctcp.Parameters = messageText;
								ctcp.Target = Nickname;
								OnCtcpMessage(ctcp);
							}
						}
						else
						{
							PublicMessage pm = new PublicMessage();
							pm.Target = channel;
							pm.Sender = nick;
							pm.Time = DateTime.Now;
							pm.Text = text;
							OnPublicMessage(pm);
						}
					}
					else
					{
						if(text.StartsWith("\x01"))
						{
							int index = text.IndexOf(' ', 1);
							index = index == -1 ? text.Length-1 : index;
							string type = text.Substring(1, index-1).ToLowerInvariant();
							string messageText = String.Empty;
							if(index != text.Length-1)
								messageText = text.Substring(index, text.Length-index-1);
							if(type.CompareTo("action") == 0)
							{
								PrivateAction pa = new PrivateAction();
								pa.Target = Nickname;
								pa.Sender = nick;
								pa.Time = DateTime.Now;
								pa.Text = messageText;
								OnPrivateAction(pa);
							}
							else
							{
								Ctcp ctcp = new Ctcp();
								ctcp.Time = DateTime.Now;
								ctcp.Sender = nick;
								ctcp.Text = type;
								ctcp.Parameters = messageText;
								ctcp.Target = Nickname;
								OnCtcpMessage(ctcp);
							}
						}
						else
						{
							PrivateMessage pm = new PrivateMessage();
							pm.Sender = nick;
							pm.Target = Nickname;
							pm.Text = text;
							pm.Time = DateTime.Now;
							OnPrivateMessage(pm);
						}
					}
				}
				#endregion
				#region private/public/ctcp notices
				else if(action.CompareTo("notice") == 0)
				{
					string target = messageParts[2];
					string text = String.Join(" ", messageParts, 3, messageParts.Length - 3).Substring(1);
					if(target.StartsWith("#") || target.StartsWith("&"))
					{
						Channel channel;
						if(!Channels.TryGetValue(target, out channel))
							Channels[target] = channel = new Channel(this, target);
						if(text.StartsWith("\x01"))
						{
							int index = text.IndexOf(' ', 1);
							index = index == -1 ? text.Length-1 : index;
							string type = text.Substring(1, index-1).ToLowerInvariant();
							string messageText = String.Empty;
							if(index != text.Length-1)
								messageText = text.Substring(index, text.Length-index-1);
							Ctcp ctcp = new Ctcp();
							ctcp.Target = Nickname;
							ctcp.Sender = nick;
							ctcp.Text = type;
							ctcp.Parameters = messageText;
							ctcp.Time = DateTime.Now;
							OnCtcpMessage(ctcp);
						}
						else
						{
							PublicNotice pn = new PublicNotice();
							pn.Target = channel;
							pn.Sender = nick;
							pn.Text = text;
							pn.Time = DateTime.Now;
							OnPublicNotice(pn);
						}
					}
					else
					{
						if(text.StartsWith("\x01"))
						{
							int index = text.IndexOf(' ', 1);
							index = index == -1 ? text.Length-1 : index;
							string type = text.Substring(1, index-1).ToLowerInvariant();
							string messageText = String.Empty;
							if(index != text.Length-1)
								messageText = text.Substring(index, text.Length-index-1);
							Ctcp ctcp = new Ctcp();
							ctcp.Sender = nick;
							ctcp.Target = Nickname;
							ctcp.Text = type;
							ctcp.Parameters = messageText;
							ctcp.Time = DateTime.Now;
							OnCtcpMessage(ctcp);
						}
						else
						{
							PrivateNotice pn = new PrivateNotice();
							pn.Sender = nick;
							pn.Target = Nickname;
							pn.Text = text;
							pn.Time = DateTime.Now;
							OnPrivateNotice(pn);
						}
					}
				}
				#endregion
				else if(action.CompareTo("quit") == 0)
				{
				}
				else if(action.CompareTo("topic") == 0)
				{
				}
				else
				{
					Message msg = new Message();
					msg.Text = "Invalid message?";
					OnDebugMessage(msg);
				}
			}
		}
		private void RegisterConnection()
		{
			string RFC1459Pass = String.Format(CultureInfo.CurrentCulture, Strings.RFC1459PassMessage, Password);
			string RFC1459User = String.Format(CultureInfo.CurrentCulture, Strings.RFC1459UserMessage, Nickname.Name, "0", "0", Nickname.Realname);
			string RFC1459Nick = String.Format(CultureInfo.CurrentCulture, Strings.NickMessage, Nickname);
			int authCount = 0;

			while(!registered)
			{
				string message = reader.ReadLine();
				if(message != null)
				{
					Message m = new Message();
					m.Text = message;
					m.Time = DateTime.Now;
					OnDebugMessage(m);
					string[] parts = message.Split(' ');
					if(parts.Length > 2 && (parts[2].ToLowerInvariant().CompareTo("auth") == 0 || parts[1].ToLowerInvariant().CompareTo("auth") == 0))
					{
						authCount++;
						if(authCount > 1)
						{
							if(Password.Length > 0)
								SendMessage(RFC1459Pass);
							SendMessage(RFC1459User);
							SendMessage(RFC1459Nick);
							registered = true;
						}
					}
				}
				Thread.Sleep(100);
			}
		}
		private void AutoPingResponder(object sender, PingEventArgs e)
		{
			SendMessage(String.Format(CultureInfo.CurrentCulture, Strings.PongMessage, e.Key));
		}
		private static bool AutoCertHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
		{
			return (sslPolicyErrors == SslPolicyErrors.None);
		}
		protected virtual void Dispose(bool disposing)
		{
			if(disposing)
			{
				reader.Close();
				writer.Close();
				connector.Close();
				connectorThread.Abort();
			}
		}
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
		#endregion
	}
}
